package com.googlecode.yoohoo.xmppcore.stanza;

import java.io.File;
import java.io.FileReader;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.osgi.framework.BundleContext;

import com.googlecode.yoohoo.utils.AppUtils;
import com.googlecode.yoohoo.utils.IoUtils;
import com.googlecode.yoohoo.xmppcore.connection.IConnectionContext;
import com.googlecode.yoohoo.xmppcore.protocol.IError;
import com.googlecode.yoohoo.xmppcore.protocol.XmppProtocolException;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.Iq;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.Message;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.Presence;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.Stanza;

public class StanzasHandler<K extends IConnectionContext> implements IStanzasHandler<K> {
	private static ConcurrentMap<Class<?>, StanzaHandlerTimeLimited> timeLimitedCache =
		new ConcurrentHashMap<Class<?>, StanzaHandlerTimeLimited>();
	private static final StanzaHandlerTimeLimited NULL_STANZA_HANDLER_TIME_LIMITED = new StanzaHandlerTimeLimited();
	
	private ReadWriteLock lock = new ReentrantReadWriteLock();
	private Lock readLock = lock.readLock();
	private Lock writeLock = lock.writeLock();
	private List<IStanzaHandlerFactory<K, Iq>> iqHandlerFactories;
	private List<IStanzaHandlerFactory<K, Message>> messageHandlerFactories;
	private List<IStanzaHandlerFactory<K, Presence>> presenceHandlerFactories;
	private List<IStanzaHandlerFactory<K, Stanza>> otherHandlerFactories;
	private Properties properties;
	private int defaultConnectionTimeout;
	
	public StanzasHandler(BundleContext bundleContext) {
		iqHandlerFactories = new ArrayList<IStanzaHandlerFactory<K, Iq>>();
		presenceHandlerFactories = new ArrayList<IStanzaHandlerFactory<K, Presence>>();
		messageHandlerFactories = new ArrayList<IStanzaHandlerFactory<K, Message>>();
		otherHandlerFactories = new ArrayList<IStanzaHandlerFactory<K, Stanza>>();
		
		defaultConnectionTimeout = 5 * 60; // 15 minutes
		String sTimeLimitedConfigFile = AppUtils.getConfigHome(bundleContext) +
				"/timelimited.config";
		properties = new Properties();
		File timeLimitedConfigFile = new File(sTimeLimitedConfigFile);
		if (timeLimitedConfigFile.exists()) {
			FileReader reader = null;
			try {
				reader = new FileReader(timeLimitedConfigFile);
				properties.load(reader);
				
				String sDefaultConnectionTimeout = (String)properties.get("default.connection.timeout");
				if (sDefaultConnectionTimeout != null) {
					defaultConnectionTimeout = Integer.parseInt(sDefaultConnectionTimeout);
				}
			} catch (Exception e) {
				// ignore
			} finally {
				IoUtils.closeIO(reader);
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see com.googlecode.yoohoo.xmppcore.stanza.IStanzasHandler#process(K, com.googlecode.yoohoo.xmppcore.protocol.stanza.Stanza)
	 */
	public boolean process(K context, Stanza stanza) {
		try {
			readLock.lock();
			
			if (stanza instanceof Message) {
				return process(context, (Message)stanza, messageHandlerFactories);
			} else if (stanza instanceof Presence) {
				return process(context, (Presence)stanza, presenceHandlerFactories);
			} else if (stanza instanceof Iq) {
				return process(context, (Iq)stanza, iqHandlerFactories);
			} else {
				return process(context, stanza, otherHandlerFactories);
			}
		} finally {
			readLock.unlock();
		}

	}
	
	@SuppressWarnings("unchecked")
	private <T extends Stanza> boolean process(K context, T stanza,
			List<IStanzaHandlerFactory<K, T>> handlerFactories) {
		Class<T> stanzaClass = null;
		Class<?>[] objectClasses = null;
		for (IStanzaHandlerFactory<K, T> handlerFactory : handlerFactories) {
			boolean matched = false;
			if (handlerFactory instanceof IStanzaTypeMatcher<?>) {
				if (stanzaClass == null) {
					stanzaClass = (Class<T>)stanza.getClass();
					if (stanza.getObjects() == null) {
						objectClasses = new Class<?>[0];
					} else {
						objectClasses = new Class<?>[stanza.getObjects().size()];
						for (int i = 0; i < stanza.getObjects().size(); i++) {
							Object object = stanza.getObjects().get(i);
							objectClasses[i] = object.getClass();
						}
					}
					
					matched = ((IStanzaTypeMatcher<T>)handlerFactory).match(stanzaClass, objectClasses);
				}
			} else { // stanza handler factory implemented IStanzaMatcher
				matched = ((IStanzaMatcher<T>)handlerFactory).match(stanza);
			}
			
			if (matched) {
				execute(context, stanza, handlerFactory.createHandler());				
				return true;
			}
		}
		
		return false;
	}

	private <T extends Stanza> void execute(K context, T stanza, IStanzaHandler<K, T> handler) {
		// TODO (rfc3921 11.1)
		//
		if (!timeLimitedCache.containsKey(handler.getClass())) {
			StanzaHandlerTimeLimited stanzaHandlerTimeLimited = getTimeLimitedFromConfig(handler);
			if (stanzaHandlerTimeLimited == null) {
				stanzaHandlerTimeLimited = getTimeLimitedFromAnnotation(handler);
			}
			
			if (stanzaHandlerTimeLimited == null)
				stanzaHandlerTimeLimited = NULL_STANZA_HANDLER_TIME_LIMITED;
			
			timeLimitedCache.putIfAbsent(handler.getClass(), stanzaHandlerTimeLimited);
		}
		
		StanzaHandlerTimeLimited stanzaHandlerTimeLimited = timeLimitedCache.get(handler.getClass());
		
		if (stanzaHandlerTimeLimited != NULL_STANZA_HANDLER_TIME_LIMITED)
			restoreTimeLimitedStates(context, stanza, stanzaHandlerTimeLimited);
		
		handler.restoreStates(context, stanza);
		
		try {
			if (handler.process(context, stanza)) {
				doCompleteThings(context, handler, stanza, stanzaHandlerTimeLimited);
			} else {
				if (stanzaHandlerTimeLimited != NULL_STANZA_HANDLER_TIME_LIMITED)
					saveTimeLimitedStates(context, stanza, stanzaHandlerTimeLimited);
				
				handler.saveStates(context, stanza);
			}
		} catch (RuntimeException e) {
			if (e instanceof XmppProtocolException) {
				IError error = ((XmppProtocolException)e).getError();
				if (error.shouldCloseStream()) {
					doCompleteThings(context, handler, stanza, stanzaHandlerTimeLimited);
				} else {
					handler.exceptionThrew(context, stanza, (XmppProtocolException)e);
				}
			} else {
				doCompleteThings(context, handler, stanza, stanzaHandlerTimeLimited);
			}

			throw e;
		}
	}

	private void saveTimeLimitedStates(IConnectionContext context, Stanza stanza,
			StanzaHandlerTimeLimited stanzaHandlerTimeLimited) {
		ConnectionTimeoutMonitorKey key = new ConnectionTimeoutMonitorKey(stanza.getId());
		((ConnectionTimeoutMonitor)context.getAttribute(key)).refreshLastConnectTime();
	}

	private <T extends Stanza> void doCompleteThings(K context, IStanzaHandler<K, T> handler, T stanza, StanzaHandlerTimeLimited stanzaHandlerTimeLimited) {
		if (stanzaHandlerTimeLimited != NULL_STANZA_HANDLER_TIME_LIMITED)
			context.removeAttribute(new ConnectionTimeoutMonitorKey(stanza.getId()));
		
		handler.complete(context, stanza);
	}

	private void restoreTimeLimitedStates(IConnectionContext context, Stanza stanza,
			StanzaHandlerTimeLimited stanzaHandlerTimeLimited) {
		ConnectionTimeoutMonitorKey key = new ConnectionTimeoutMonitorKey(stanza.getId());
		if (context.getAttribute(key) == null) {
			if (stanzaHandlerTimeLimited.timeout == -1) {
				stanzaHandlerTimeLimited.timeout = defaultConnectionTimeout;
			}
			
			ConnectionTimeoutMonitor value = new ConnectionTimeoutMonitor(stanzaHandlerTimeLimited.timeout,
					stanzaHandlerTimeLimited.shouldCloseStream);
			
			context.setAttribute(key, value);
		}
	}

	private StanzaHandlerTimeLimited getTimeLimitedFromAnnotation(IStanzaHandler<?, ?> handler) {
		TimeLimited timeLimited = handler.getClass().getAnnotation(TimeLimited.class);
		
		if (timeLimited == null)
			return null;
		
		StanzaHandlerTimeLimited stanzaHandlerTimeLimited = new StanzaHandlerTimeLimited();
		stanzaHandlerTimeLimited.timeout = timeLimited.timeout();
		stanzaHandlerTimeLimited.shouldCloseStream = timeLimited.shouldCloseStream();
		
		return stanzaHandlerTimeLimited;
	}

	private StanzaHandlerTimeLimited getTimeLimitedFromConfig(IStanzaHandler<?, ?> handler) {
		String sTimeout = properties.getProperty(handler.getClass().getName() + ".timeout");
		String sShouldCloseStream = properties.getProperty(handler.getClass().getName() + ".shouldclosestream");
		
		if (sTimeout == null && sShouldCloseStream == null)
			return null;
		
		StanzaHandlerTimeLimited stanzaHandlerTimeLimited = new StanzaHandlerTimeLimited();
		stanzaHandlerTimeLimited.timeout = -1;
		stanzaHandlerTimeLimited.shouldCloseStream = true;
		
		if (sTimeout != null) {
			try {
				stanzaHandlerTimeLimited.timeout = Integer.parseInt(sTimeout);
			} catch (Exception e) {
				return null;
			}
		}
		
		if (sShouldCloseStream != null) {
			try {
				stanzaHandlerTimeLimited.shouldCloseStream = Boolean.parseBoolean(sShouldCloseStream);
			} catch (Exception e) {
				return null;
			}
		}
		
		return stanzaHandlerTimeLimited;
	}

	/* (non-Javadoc)
	 * @see com.googlecode.yoohoo.xmppcore.stanza.IStanzasHandler#registerHandler(com.googlecode.yoohoo.xmppcore.stanza.IStanzaHandlerFactory)
	 */
	@SuppressWarnings("unchecked")
	public void registerHandler(IStanzaHandlerFactory<K, ? extends Stanza> handlerFactory) {
		if (!(handlerFactory instanceof IStanzaMatcher) && !(handlerFactory instanceof IStanzaTypeMatcher)) {
			throw new IllegalArgumentException("Stanza handler factory must implement either IStanzaMatcher or IStanzaTypeMatcher.");
		}
		
		try {
			writeLock.lock();
			Class<?> clazz = getStanzaType(handlerFactory);
			if (clazz == null) {
				return;
			}
			
			if (Message.class.isAssignableFrom(clazz)) {
				messageHandlerFactories.add((IStanzaHandlerFactory<K, Message>)handlerFactory);
			} else if (Presence.class.isAssignableFrom(clazz)) {
				presenceHandlerFactories.add((IStanzaHandlerFactory<K, Presence>)handlerFactory);
			} else if (Iq.class.isAssignableFrom(clazz)) {
				iqHandlerFactories.add((IStanzaHandlerFactory<K, Iq>)handlerFactory);
			} else {
				otherHandlerFactories.add((IStanzaHandlerFactory<K, Stanza>)handlerFactory);
			}
		} finally {
			writeLock.unlock();
		}
	
	}

	private Class<?> getStanzaType(IStanzaHandlerFactory<K, ? extends Stanza> handlerFactory) {
		Type[] genericTypes = handlerFactory.getClass().getGenericInterfaces();
		
		for (Type genericType : genericTypes) {
			if (genericType instanceof ParameterizedType) {
				ParameterizedType parameterizedType = (ParameterizedType)genericType;
				Type[] classTypes = parameterizedType.getActualTypeArguments();
				if (classTypes.length > 0 && classTypes[0] instanceof Class<?>) {
					return (Class<?>)classTypes[0];
				}
			}
		}
		
		return null;
	}
	
	/* (non-Javadoc)
	 * @see com.googlecode.yoohoo.xmppcore.stanza.IStanzasHandler#unregisterHandler(com.googlecode.yoohoo.xmppcore.stanza.IStanzaHandlerFactory)
	 */
	public void unregisterHandler(IStanzaHandlerFactory<K, ? extends Stanza> handlerFactory) {
		try {
			writeLock.lock();
			Class<?> clazz = getStanzaType(handlerFactory);
			if (clazz == null) {
				return;
			}
			
			if (Message.class.isAssignableFrom(clazz)) {
				messageHandlerFactories.remove(handlerFactory);
			} else if (Presence.class.isAssignableFrom(clazz)) {
				presenceHandlerFactories.remove(handlerFactory);
			} else if (Iq.class.isAssignableFrom(clazz)) {
				iqHandlerFactories.remove(handlerFactory);
			} else {
				otherHandlerFactories.remove(handlerFactory);
			}
		} finally {
			writeLock.unlock();
		}
	}
	
	private static class StanzaHandlerTimeLimited {
		public int timeout;
		public boolean shouldCloseStream;
	}
	
}
